异步回调函数与Promise
实际开发中经常会遇到这样的需求:从A请求获取到的数据,得到的结果传给B请求,按照ajax的回调函数写法应该是这样的:
1 | $.ajax({ |
以上请求存在的问题:
- 1.请求之间依赖关系,造成ajax请求嵌套形成回调地狱,代码难以维护,可读性极差。
- 2.错误处理的代码与正常的业务代码耦合在一起,造成代码非常难看。
由于以上原因,ES6中的Promise提供了这样的异步回调函数解决方案,先来看promise的兼容性情况:
关于Promise的创建及使用,在Promise应用场景与模块化尝试已经做了足够的描述,此处不再赘述。
Promise是解决异步回调的一种方式,每一个then方法都会返回一个新建的Promise对象,无论你的异步操作成功与否,这个对象都会返回一个值。Promise链写的代码,比层层调用回调函数更优雅,流程也更明确。先获得数据库对象,再获得集合对象,最后查询数据。
Promise存在的问题
每一个then()方法获取的对象,都是由上一个返回的数据,并且不可以跨层访问。在Node.js端mongodb js驱动默认返回Promise,以此使用Promise链为例:
1 |
|
第三个then方法只能获取到查询的结果blogs,而不能使用上级的db对象及coll对象。这时,如果需要打印出blogs列表后,关闭数据库db.closer()。而如果要达到这个目的,可以使用两个解决方案:
一,使用then()嵌套。将Promise链打断,使其嵌套,与回调函数的嵌套相同:
1 | MongoClient.connect(url + db_name).then(db=> { |
显然这并不是一种很好的解决办法,从ajax的回调地狱进入到了Promise回调地狱的泥沼,并且需要对每一个Promise捕捉异常,Promise并没有形成链。
二,在每个then()方法都将db传入:
1 | MongoClient.connect(url + db_name).then(db=> { |
在以上的例子中可以看出,如果返回值是一个Promise值,那每一个Promise都需要在进行解析。{db:result.db,blogs:result.coll.find().toArray()}
对象中,blogs是一个Promise值,因此需要先用then方法解析一层,再将同步值db和blogs返回。这里涉及到了Promise的嵌套,不过一个Promise只嵌套了一层then()。
从以上两种解决方法来看,如果一定要使用Promise链,都不是最优的方法。
async/await解决方案
如果使用async/await改写以上的代码:1
2
3
4
5
6
7
8
9(async function(){
let db = await MongoClient.connect(url + db_name);
let coll = db.collection("blogs");
let blogs = await coll.find().toArray();
console.log(blogs.length);
db.close();
})().catch(err=>{
console.log(err);
})
代码中的async表明函数是异步的,同时await表示要等待异步操作返回值,而async其实就是Promise的一个语法糖,通过async/await函数,异步请求可以变得更直观优雅,接下来继续深入了解ES中的新特性。
深入async/await
关于async/await函数的兼容性,目前来说,浏览器兼容程度较之Promise来说还不是很高:
根据文档定义,async返回的就是一个Promise对象,所以在最外层不能使用await获取到其返回值时,可以使用then()解析这个Promise对象。如果async函数没有返回值,它返回的就是Promise.resolve(undefined)
。联想到Promise的一个特点—立即执行,所以并不会阻塞之后的语句,这与普通返回的Promise对象并没有不同。
所以,还是在于await关键字上。那么,await等待的到底是什么呢?
await不仅等待Promise对象,可以等任何表达式的结果。 所以,await后可以是普通函数调用或直接量。如下例子:
1 | function sleep(second){ |
await等到了想要的东西,一个Promise对象或其他值,如果等到的是Promise对象,await就会阻塞后面的代码,等待Promise被解析完成得到resolve值,作为await表达式的运算结果。
关于阻塞,这就是await必须放在async函数中的原因。async函数调用不会造成阻塞,因为内部所有的阻塞都被封装在了Promise对象中异步执行。
async/await的优势
单一的Promise链并没有看出来async/await的优势,但是如果需要处理多个Promise形成的then链时,async/await函数的优点就能凸显无疑。
例如,有三个请求按时间顺序分别为A、B、C。B依赖于A的结果返回解构,C依赖于B的请求结果结构。如果使用回调函数会有3层回调,Promise则会有三个then,同样会是嵌套的Promise。
1 | function sleep(second, param) { |
而如果使用async/await改造该方法,从可读性和易维护上就提升了许多。
1 | //...sleep() |
错误处理
因为async/await不再需要使用then()解析,因此也就不存在使用链式的catch捕捉错误,直接用try/catch包裹就能捕捉错误。
例如:
function sleep(second) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('want to sleep~');
}, second);
})
}
async function errorDemo() {
let result = await sleep(1000);
console.log(result);
}
errorDemo();// VM706:11 Uncaught (in promise) want to sleep~
// 为了处理Promise.reject 的情况,应该将代码块用 try catch 包裹一下
async function errorDemoSuper() {
try {
let result = await sleep(1000);
console.log(result);
} catch (err) {
console.log(err);
}
}
并行
对于没有相互关联性的并发请求,并不需要将其放入await中阻塞请求进程。例如A、B、C三个异步请求结束之后清除加载动画,并不需要将以上三个需求放置在await中,而应该是这样:
function sleep(second) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('request done! ' + Math.random());
}, second);
})
}
async function Demo(){
let p1 = sleep(1000);
let p2 = sleep(1000);
let p3 = sleep(2000);
await Promise.all([p1,p2,p3]);
console.log("clear the loading");
}
Demo();